/*
 * sockets-unix.c
 *
 *  Created on: May 19, 2012
 *      Author: burek
 */

#include "sockets.h"
#include "threading.h"

#ifndef WIN32

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>

#define CUS_NIX_BIND_ERROR (1)
#define CUS_NIX_CANT_CREATE_SOCKET (2)
#define CUS_NIX_LISTEN_ERROR (3)
#define CUS_NIX_INVALID_ADDRESS_FAMILY (4)
#define CUS_NIX_INVALID_ADDRESS (5)
#define CUS_NIX_CONNECT_FAIL (6)
#define CUS_NIX_ACCEPT_FAIL (7)
#define CUS_NIX_CLOSE_FAIL (8)

int __sockError = 0;

CUSocket * cu_createTCPSocket()
{
	int SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

	if(-1 == SocketFD)
	{
		__sockError = CUS_NIX_CANT_CREATE_SOCKET;
		return NULL;
	}
	else
	{
		CUSocket * sock = (CUSocket*)malloc(sizeof(CUSocket));
		sock->nix_socketDesc = SocketFD;
		return sock;
	}
}

CUSocket * cu_createUDPSocket(int port)
{
	int sock;

	if ((sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
	{
		__sockError = CUS_NIX_CANT_CREATE_SOCKET;
		return NULL;
	}
	else
	{
		CUSocket * cusock = (CUSocket*)malloc(sizeof(CUSocket));

		memset(&cusock->hostAddr, 0, sizeof(cusock->hostAddr));
		cusock->hostAddr.sin_family = AF_INET;
		cusock->hostAddr.sin_addr.s_addr = htonl(INADDR_ANY);
		cusock->hostAddr.sin_port = htons(port);

		if (bind(sock, (struct sockaddr *) &(cusock->hostAddr), sizeof(cusock->hostAddr)) < 0)
		{
			__sockError = CUS_NIX_BIND_ERROR;
			free(cusock);
			return NULL;
		}

		cusock->nix_socketDesc = sock;
		cusock->port = port;
		cusock->protocol = CU_UDP;
		return cusock;
	}
}

int cu_openServerSocket(CUSocket * sock, int port)
{
	struct sockaddr_in stSockAddr;
	int iSetOption = 1;

	if(sock == NULL) return CU_ERROR;
 	memset(&stSockAddr, 0, sizeof(stSockAddr));

	stSockAddr.sin_family = AF_INET;
	stSockAddr.sin_port = htons(port);
	stSockAddr.sin_addr.s_addr = INADDR_ANY;

	setsockopt(sock->nix_socketDesc, SOL_SOCKET, SO_REUSEADDR, (char*)&iSetOption, sizeof(iSetOption));

	if(-1 == bind(sock->nix_socketDesc, (struct sockaddr *)&stSockAddr, sizeof(stSockAddr)))
	{
		__sockError = CUS_NIX_BIND_ERROR;

		switch(errno)
		{
		case EADDRINUSE:
			puts("The specified address is already in use.");
			break;
		case EADDRNOTAVAIL:
			puts("The specified address is not available from the local machine.");
			break;
		case EAFNOSUPPORT:
			puts("The specified address is not a valid address for the address family of the specified socket.");
			break;
		case EBADF:
			puts("The socket argument is not a valid file descriptor.");
			break;
		case EINVAL:
			puts("The socket is already bound to an address, and the protocol does not support binding to a new address; or the socket has been shut down.");
			break;
		case ENOTSOCK:
			puts("The socket argument does not refer to a socket.");
			break;
		case EOPNOTSUPP:
			puts("The socket type of the specified socket does not support binding to an address.");
			break;
		default:
			puts("lolwut?"); break;
		}
		fflush(stdout);

		close(sock->nix_socketDesc);
		return CU_ERROR;
	}

	if(-1 == listen(sock->nix_socketDesc, MAX_PENDING))
	{
		__sockError = CUS_NIX_LISTEN_ERROR;
		close(sock->nix_socketDesc);
		return CU_ERROR;
	}

	return CU_OK;
}

int cu_openClientSocket(CUSocket * sock, const char * address, int port)
{
	struct sockaddr_in stSockAddr;
	int Res;
	if(sock == NULL) return CU_ERROR;

	memset(&stSockAddr, 0, sizeof(stSockAddr));

	stSockAddr.sin_family = AF_INET;
	stSockAddr.sin_port = htons(port);
	Res = inet_pton(AF_INET, address, &stSockAddr.sin_addr);

	if (0 > Res)
	{
		__sockError = CUS_NIX_INVALID_ADDRESS_FAMILY;
		close(sock->nix_socketDesc);
		return CU_ERROR;
	}
	else if (0 == Res)
	{
		__sockError = CUS_NIX_INVALID_ADDRESS;
		close(sock->nix_socketDesc);
		return CU_ERROR;
	}

	if (-1 == connect(sock->nix_socketDesc, (struct sockaddr *)&stSockAddr, sizeof(stSockAddr)))
	{
		__sockError = CUS_NIX_CONNECT_FAIL;
		close(sock->nix_socketDesc);
		return CU_ERROR;
	}

	sock->port = port;
	strcpy(sock->ipTxt, address);
	sock->ipAddr = stSockAddr.sin_addr.s_addr;
	return CU_OK;
}

CUSocket * cu_acceptConnection(CUSocket * sock)
{
	struct sockaddr_in client_info = {0};
	unsigned int size = sizeof(client_info);
	int ConnectFD = accept(sock->nix_socketDesc, (struct sockaddr *)&client_info, &size);

	if(0 > ConnectFD)
	{
		switch(errno)
		{
		case EBADF: puts("The socket argument is not a valid file descriptor."); fflush(stdout); break;
		case ENOTSOCK: puts("The descriptor socket argument is not a socket."); fflush(stdout); break;
		case EOPNOTSUPP: puts("The descriptor socket does not support this operation."); fflush(stdout); break;
		case EWOULDBLOCK: puts("socket has nonblocking mode set, and there are no pending connections immediately available."); fflush(stdout); break;
		default: printf("lolwut? %d\n", ConnectFD); fflush(stdout); break;
		}

		__sockError = CUS_NIX_ACCEPT_FAIL;
		close(sock->nix_socketDesc);
		return NULL;
	}
	else
	{
		CUSocket * con = (CUSocket*)malloc(sizeof(CUSocket));
		con->ipAddr = client_info.sin_addr.s_addr;
		con->port = client_info.sin_port;
		sprintf(con->ipTxt, "%d.%d.%d.%d",
				con->ipAddr & 0xFF,
				(con->ipAddr >> 8) & 0xFF,
				(con->ipAddr >> 16) & 0xFF,
				(con->ipAddr >> 24) & 0xFF);

		con->nix_socketDesc = ConnectFD;
		return con;
	}
}

int cu_closeSocket(CUSocket * sock)
{
	if(sock == NULL) return CU_ERROR;
	if (-1 == shutdown(sock->nix_socketDesc, SHUT_RDWR))
	{
		__sockError = CUS_NIX_CLOSE_FAIL;
		close(sock->nix_socketDesc);
		return CU_ERROR;
	}

	close(sock->nix_socketDesc);
	return CU_OK;
}

int cu_sendData(CUSocket * sock, const char * buffer, int length)
{
	if(sock == NULL) return CU_ERROR;
	if(length == 0) length = strlen(buffer) + 1;
	return write(sock->nix_socketDesc, (const void *) buffer, length);
}

int cu_recvData(CUSocket * sock, char * buffer, int length)
{
	if(sock == NULL) return CU_ERROR;
	return read(sock->nix_socketDesc, (void*) buffer, length);
}

int cu_sendDataToIP(CUSocket * sock, const char * buffer, int length, unsigned int ip, int port)
{
	if(sock == NULL) return CU_ERROR;
	if(length == 0) length = strlen(buffer) + 1;
	if(sock->protocol == CU_UDP)
	{
		struct sockaddr_in dest;
		dest.sin_family = AF_INET;
		dest.sin_addr.s_addr = ip;
		dest.sin_port = htons(port);
		return sendto(sock->nix_socketDesc, buffer, length, 0, (const struct sockaddr *) &dest, sizeof(dest));
	}
	else
	{
		return -1;
	}
}

int cu_sendDataTo(CUSocket * sock, const char * buffer, int length, const char * ip, int port)
{
	return cu_sendDataToIP(sock, buffer, length, inet_addr(ip), port);
}

int cu_recvSpin(CUSocket * sock, char * buffer, int length, int cycle, int count)
{
	int i = 0;
	if(sock == NULL) return CU_ERROR;
	for(i = 0; i < count; ++i)
	{
		cu_sleep(cycle);
		if(recv(sock->nix_socketDesc, buffer, length, MSG_PEEK) > 0)
		{
			return read(sock->nix_socketDesc, (void*) buffer, length);
		}
	}
	return CU_ERROR;
}

int cu_recvDataFrom(CUSocket * sock, char * buffer, int length, unsigned int * ip, char * ipTxt, int * port)
{
	int result = 0;
	struct sockaddr_in SenderAddr;
	unsigned int SenderAddrSize = sizeof(SenderAddr);

	if(sock == NULL)
		return CU_ERROR;
	if(sock->protocol != CU_UDP)
		return CU_ERROR;

	result = recvfrom(sock->nix_socketDesc,
	                       buffer, length, 0, (struct sockaddr *) & SenderAddr, &SenderAddrSize);

	if(result >= 0)
	{
		if(ip != NULL) *ip = SenderAddr.sin_addr.s_addr;
		if(port != NULL) *port = ntohs(SenderAddr.sin_port);
		if(ipTxt != NULL)
		{
			sprintf(ipTxt, "%d.%d.%d.%d",
					SenderAddr.sin_addr.s_addr & 0xFF,
							(SenderAddr.sin_addr.s_addr >> 8) & 0xFF,
							(SenderAddr.sin_addr.s_addr >> 16) & 0xFF,
							(SenderAddr.sin_addr.s_addr >> 24) & 0xFF);
		}
	}

	return result;
}

const char * cu_getSocketError()
{
	switch(__sockError)
	{
	case 0:
		return "No error"; break;
	case CUS_NIX_ACCEPT_FAIL:
		return "Accepting connection failed"; break;
	case CUS_NIX_BIND_ERROR:
		return "Socket binding failed"; break;
	case CUS_NIX_CANT_CREATE_SOCKET:
		return "Cannot create socket"; break;
	case CUS_NIX_CLOSE_FAIL:
		return "Closing socket failed"; break;
	case CUS_NIX_CONNECT_FAIL:
		return "Connection failed"; break;
	case CUS_NIX_INVALID_ADDRESS:
		return "Invalid address"; break;
	case CUS_NIX_INVALID_ADDRESS_FAMILY:
		return "Invalid address family"; break;
	case CUS_NIX_LISTEN_ERROR:
		return "Error listening"; break;
	default:
		return "Unknown error"; break;
	}
	return "";
}

void cu_clearSocketError()
{
	__sockError = 0;
}

unsigned int cu_resolveDomain(const char * dname)
{
	unsigned long hostaddr = inet_addr( dname );

	if( hostaddr == INADDR_NONE )
	{
		//int i;
		struct hostent *host = gethostbyname(dname);
		if(host == NULL)
			return 0;
		hostaddr = *(unsigned long *)( host->h_addr_list[0] );
		//for(i = 0; host->h_addr_list[i] != NULL; ++i)
		//{
			//printf("Resolve[%d] = %d.%d.%d.%d\n", i, (unsigned char)host->h_addr_list[i][0],
					//(unsigned char)host->h_addr_list[i][1], (unsigned char)host->h_addr_list[i][2],
					//(unsigned char)host->h_addr_list[i][3]);
		//}
		return hostaddr;
	}
	else
	{
		return 0;
	}
}

#endif
